﻿namespace Agility.ViewModels
{
    using System;
    using System.Linq;
    using System.ComponentModel.DataAnnotations;
    using System.ServiceModel.DomainServices.Client;
    using System.ServiceModel.DomainServices.Client.ApplicationServices;
    using Agility.Web.Resources;
    using Agility.Web.Services;
    using Agility.Web.Models.RegistrationForm;
    using System.ComponentModel;
    using Agility.Web;
    using Agility.ViewModels.Resources;
    using System.Threading;

    /// <summary>
    /// Extensions to provide client side custom validation and data binding to <see cref="RegistrationFormViewModel"/>.
    /// </summary>
    public class RegistrationFormViewModel : ComplexObject
    {
        private OperationBase currentOperation;
        private UserRegistrationContext userRegistrationServiceClient;
        private AuthenticationService authenticationService;
        private RegistrationData registrationData;
        private bool? dialogResult;

        public event EventHandler<RegistrationCompletedEventArgs> RegistrationCompleted;

        public RegistrationFormViewModel(AuthenticationService authenticationService)
        {
            this.userRegistrationServiceClient = new UserRegistrationContext();
            Initialize(authenticationService);
        }

        /// <summary>
        /// Constructor used for testing purposes
        /// </summary>
        /// <param name="authenticationService">the AuthenticationService to use</param>
        /// <param name="domainClient">the DomainClient to use</param>
        internal RegistrationFormViewModel(AuthenticationService authenticationService, DomainClient domainClient)
        {
            this.userRegistrationServiceClient = new UserRegistrationContext(domainClient);
            Initialize(authenticationService);
        }

        private void Initialize(AuthenticationService authenticationService)
        {
            this.registrationData = new RegistrationData();
            this.authenticationService = authenticationService;
        }

        #region Properties
        /// <summary>
        /// Gets and sets the answer to the security question.
        /// </summary>
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [Display(Order = 6, Name = "SecurityAnswerLabel", ResourceType = typeof(RegistrationDataResources))]
        [StringLength(128, ErrorMessageResourceName = "ValidationErrorBadAnswerLength", ErrorMessageResourceType = typeof(ValidationErrorResources))] 
        public string Answer
        {
            get { return this.registrationData.Answer; }
            set 
            {
                this.ValidateProperty("Answer", value);
                if (this.registrationData.Answer != value)
                {
                    this.registrationData.Answer = value;
                    this.RaisePropertyChanged("Answer");
                }
            }
        }

        /// <summary>
        /// Gets and sets the user name.
        /// </summary>
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [Display(Order = 0, Name = "UserNameLabel", ResourceType = typeof(RegistrationDataResources))]
        [RegularExpression("^[a-zA-Z0-9_\\-]*$", ErrorMessageResourceName = "ValidationErrorInvalidUserName", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [StringLength(255, MinimumLength = 4, ErrorMessageResourceName = "ValidationErrorBadUserNameLength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        public string UserName 
        {
            get { return this.registrationData.UserName; }
            set 
            {
                this.ValidateProperty("UserName", value);
                if (this.registrationData.UserName != value)
                {
                    this.registrationData.UserName = value;
                    this.RaisePropertyChanged("UserName");
                }
            }
        }

        /// <summary>
        /// Gets and sets the email address.
        /// </summary>
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [Display(Order = 2, Name = "EmailLabel", ResourceType = typeof(RegistrationDataResources))]
        [RegularExpression(@"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$",
                           ErrorMessageResourceName = "ValidationErrorInvalidEmail", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        public string Email 
        {
            get { return this.registrationData.Email; }
            set 
            {
                this.ValidateProperty("Email", value);
                if (this.registrationData.Email != value)
                {
                    this.registrationData.Email = value;
                    this.RaisePropertyChanged("Email");
                }
            }
        }

        /// <summary>
        /// Gets and sets the friendly name of the user.
        /// </summary>
        [Display(Order = 1, Name = "FriendlyNameLabel", Description = "FriendlyNameDescription", ResourceType = typeof(RegistrationDataResources))]
        [StringLength(255, MinimumLength = 0, ErrorMessageResourceName = "ValidationErrorBadFriendlyNameLength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        public string FriendlyName 
        {
            get { return this.registrationData.FriendlyName; }
            set 
            {
                this.ValidateProperty("FriendlyName", value);
                if (this.registrationData.FriendlyName != value)
                {
                    this.registrationData.FriendlyName = value;
                    this.RaisePropertyChanged("FriendlyName");
                }
            }
        }

        /// <summary>
        /// Gets and sets the security question.
        /// </summary>
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [Display(Order = 5, Name = "SecurityQuestionLabel", ResourceType = typeof(RegistrationDataResources))]
        public string Question 
        {
            get { return this.registrationData.Question; }
            set 
            {
                this.ValidateProperty("Question", value);
                if (this.registrationData.Question != value)
                {
                    this.registrationData.Question = value;
                    this.RaisePropertyChanged("Question");
                }
            }
        }

        /// <summary>
        /// Gets or sets a function that returns the password.
        /// </summary>
        internal Func<string> PasswordAccessor { get; set; }

        /// <summary>
        /// Gets and sets the password.
        /// </summary>
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [Display(Order = 3, Name = "PasswordLabel", Description = "PasswordDescription", ResourceType = typeof(RegistrationDataResources))]
        [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*[\d])(?=.*[\W]).*$", ErrorMessageResourceName = "ValidationErrorBadPasswordStrength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [StringLength(50, MinimumLength = 8, ErrorMessageResourceName = "ValidationErrorBadPasswordLength", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        public string Password
        {
            get
            {
                return (this.PasswordAccessor == null) ? string.Empty : this.PasswordAccessor();
            }

            set
            {
                this.ValidateProperty("Password", value);
                this.CheckPasswordConfirmation();

                // Do not store the password in a private field as it should not be stored in memory in plain-text.
                // Instead, the supplied PasswordAccessor serves as the backing store for the value.

                this.RaisePropertyChanged("Password");
            }
        }

        /// <summary>
        /// Gets or sets a function that returns the password confirmation.
        /// </summary>
        internal Func<string> PasswordConfirmationAccessor { get; set; }

        /// <summary>
        /// Gets and sets the password confirmation string.
        /// </summary>
        [Required(ErrorMessageResourceName = "ValidationErrorRequiredField", ErrorMessageResourceType = typeof(ValidationErrorResources))]
        [Display(Order = 4, Name = "PasswordConfirmationLabel", ResourceType = typeof(RegistrationDataResources))]
        public string PasswordConfirmation
        {
            get
            {
                return (this.PasswordConfirmationAccessor == null) ? string.Empty : this.PasswordConfirmationAccessor();
            }

            set
            {
                this.ValidateProperty("PasswordConfirmation", value);
                this.CheckPasswordConfirmation();

                // Do not store the password in a private field as it should not be stored in memory in plain-text.  
                // Instead, the supplied PasswordAccessor serves as the backing store for the value.

                this.RaisePropertyChanged("PasswordConfirmation");
            }
        }

        /// <summary>
        /// Gets the current registration or login operation.
        /// </summary>
        /// 
        [Display(AutoGenerateField = false)]
        public OperationBase CurrentOperation
        {
            get
            {
                return this.currentOperation;
            }
            set
            {
                if (this.currentOperation != value)
                {
                    if (this.currentOperation != null)
                    {
                        this.currentOperation.Completed -= (s, e) => this.CurrentOperationChanged();
                    }

                    this.currentOperation = value;

                    if (this.currentOperation != null)
                    {
                        this.currentOperation.Completed += (s, e) => this.CurrentOperationChanged();
                    }

                    this.CurrentOperationChanged();
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether the user is presently being registered or logged in.
        /// </summary>
        [Display(AutoGenerateField = false)]
        public bool IsRegistering
        {
            get
            {
                return this.CurrentOperation != null && !this.CurrentOperation.IsComplete;
            }
        }

        /// <summary>
        /// Gets the result of the combined user creation/login action
        /// </summary>
        [Display(AutoGenerateField=false)]
        public bool? DialogResult 
        {
            get { return dialogResult; }
            private set
            {
                dialogResult = value;
                this.RaisePropertyChanged("DialogResult");
            }
        }

        #endregion
        #region Methods
        protected virtual void OnRegistrationCompleted(bool registrationResult, bool hasValidationErros, string errorMessage, Exception error)
        {
            if (this.RegistrationCompleted != null)
            {
                RegistrationCompletedEventArgs eventArgs = new RegistrationCompletedEventArgs(registrationResult, hasValidationErros, errorMessage, error);
                RegistrationCompleted(this, eventArgs);
            }
        }

        /// <summary>
        /// If a registration or login operation is in progress and is cancellable, cancel it.
        /// </summary>
        public bool CancelCurrentOperation()
        {
            if (this.CurrentOperation != null && this.CurrentOperation.CanCancel)
            {
                this.CurrentOperation.Cancel();
                return true;
            }
            else
                return false;
        }

        /// <summary>
        /// Creates a user account using the data provided to the view model
        /// </summary>
        public void RegisterUserAsync()
        {

            this.CurrentOperation = this.userRegistrationServiceClient.CreateUser(registrationData, this.Password,
                                                                                     this.CreateUser_Completed, null);
        }

        /// <summary>
        /// Completion handler for the registration operation. 
        /// If there were validation errors, they are added to the validation errors of the viewmodel.
        /// Otherwise, this triggers a login operation that will automatically log in the just registered user.
        /// </summary>
        private void CreateUser_Completed(InvokeOperation<CreateUserStatus> operation)
        {
            if (!operation.IsCanceled)
            {
                if (operation.HasError)
                {
                    bool hasValidationErrors = false;
                    if (operation.ValidationErrors.Count() > 0)
                    {
                        // This only works as long as the properties of the ViewModel and Model have the same names
                        this.ValidationErrors.ToList().AddRange(operation.ValidationErrors);
                        hasValidationErrors = true;
                    }
                    operation.MarkErrorAsHandled();
                    OnRegistrationCompleted(false, hasValidationErrors, operation.Error.Message, operation.Error);
                }
                else if (operation.Value == CreateUserStatus.Success)
                {
                    this.CurrentOperation = this.authenticationService.Login(this.ToLoginParameters(), this.Login_Completed, null);
                }
                else if (operation.Value == CreateUserStatus.DuplicateEmail)
                {
                    this.ValidationErrors.Add(new ValidationResult(ValidationErrorResources.CreateUserStatusDuplicateEmail, new string[] { "Email" }));
                    this.OnRegistrationCompleted(false, true, ValidationErrorResources.CreateUserStatusDuplicateEmail, null);
                }
                else if (operation.Value == CreateUserStatus.DuplicateUserName)
                {
                    this.ValidationErrors.Add(new ValidationResult(ValidationErrorResources.CreateUserStatusDuplicateUserName, new string[] { "UserName" }));
                    this.OnRegistrationCompleted(false, true, ValidationErrorResources.CreateUserStatusDuplicateUserName, null);
                }
                else if (operation.Value == CreateUserStatus.Failure)
                {
                    this.OnRegistrationCompleted(false, false, ValidationErrorResources.CreateUserStatusFailed, null);
                }
            }
        }

        /// <summary>
        /// Completion handler for the login operation that occurs after a successful registration and login attempt.
        /// This will close the window. If the operation fails, an <see cref="ErrorWindow"/> will display the error message.
        /// </summary>
        /// <param name="loginOperation">The <see cref="LoginOperation"/> that has completed.</param>
        private void Login_Completed(LoginOperation loginOperation)
        {
            if (!loginOperation.IsCanceled)
            {
                this.DialogResult = true;

                if (loginOperation.HasError)
                {

                    OnRegistrationCompleted(false, false, loginOperation.Error.Message, loginOperation.Error);
                    loginOperation.MarkErrorAsHandled();
                }
                else if (loginOperation.LoginSuccess == false)
                    // The operation was successful, but the actual login was not
                    this.OnRegistrationCompleted(false, false, ErrorResources.ErrorLoginAfterRegistrationFailed, null);
                else
                    OnRegistrationCompleted(true, false, null, null);
            }
        }

        /// <summary>
        /// Helper method for when the current operation changes.
        /// Used to raise appropriate property change notifications.
        /// </summary>
        private void CurrentOperationChanged()
        {
            this.RaisePropertyChanged("IsRegistering");
        }

        /// <summary>
        /// Checks to ensure the password and confirmation match.
        /// If they don't match, a validation error is added.
        /// </summary>
        private void CheckPasswordConfirmation()
        {
            // If either of the passwords has not yet been entered, then don't test for equality between the fields.
            // The Required attribute will ensure a value has been entered for both fields.
            if (string.IsNullOrWhiteSpace(this.Password)
                || string.IsNullOrWhiteSpace(this.PasswordConfirmation))
            {
                return;
            }

            // If the values are different, then add a validation error with both members specified
            if (this.Password != this.PasswordConfirmation)
            {
                this.ValidationErrors.Add(new ValidationResult(ValidationErrorResources.ValidationErrorPasswordConfirmationMismatch, new string[] { "PasswordConfirmation", "Password" }));
            }
        }

        /// <summary>
        /// Perform logic after the UserName value has been entered.
        /// </summary>
        /// <param name="userName">The user name value that was entered.</param>
        /// <remarks>
        /// Allow the form to indicate when the value has been completely entered.
        /// Using the OnUserNameChanged method can lead to a premature call before the user has finished entering the value in the form.
        /// </remarks>
        internal void UserNameEntered(string userName)
        {
            // Auto-Fill FriendlyName to match UserName for new entities when there is not a friendly name specified
            if (string.IsNullOrWhiteSpace(this.FriendlyName))
            {
                this.FriendlyName = userName;
            }
        }

        /// <summary>
        /// Creates a new <see cref="LoginParameters"/> initialized with this entity's data (IsPersistent will default to false).
        /// </summary>
        public LoginParameters ToLoginParameters()
        {
            return new LoginParameters(this.UserName, this.Password, false, null);
        }

        #endregion
    }
}
